import xml.etree.ElementTree as ET
import requests
import platform
import subprocess
import os
from typing import List, Dict
import tkinter as tk
from tkinter import ttk
import threading
import concurrent.futures
import queue
from datetime import datetime
import ctypes

APP_NAME = "MPLAB One-Stop Installer"
APP_VERSION = "0.1.2"
APP_AUTHOR = "BeLLsTuDio"

class MplabInstaller:
    def __init__(self):
        self.feed_url = "https://www.microchip.com/bin/mchp/mplabxfeed.xml"

        # Set default download directory to user's Downloads folder
        # Detect the OS type
        current_os = platform.system()
        if current_os == "Linux":
            downloads_dir = os.path.join(os.path.expanduser("~"), "Downloads")
        elif current_os == "Darwin":
            downloads_dir = os.path.join(os.path.expanduser("~"), "Downloads")
        else: # Windows
            # This is a Windows-specific implementation
            try:
                import winreg
                with winreg.OpenKey(winreg.HKEY_CURRENT_USER, 
                                r'SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders') as key:
                    downloads_dir = winreg.QueryValueEx(key, '{374DE290-123F-4565-9164-39C4925E467B}')[0]
            except:
                # Fallback to user's home directory Downloads folder
                downloads_dir = os.path.join(os.path.expanduser("~"), "Downloads")
        
        self.downloads_dir = os.path.join(downloads_dir, "MPLAB")
        os.makedirs(self.downloads_dir, exist_ok=True)
        self.download_queue = queue.Queue()
        self.install_queue = queue.Queue()

    def fetch_feed(self) -> ET.Element:
        """Fetch and parse the MPLAB X feed XML"""
        response = requests.get(self.feed_url)
        return ET.fromstring(response.content)

    def get_available_products(self) -> Dict[str, List[Dict]]:
        """Get list of available products and their versions"""
        root = self.fetch_feed()
        products = {}
        
        for item in root.findall(".//item"):
            product = item.find("product").text
            os_type = item.find("os").text
            current_os = platform.system()
            if current_os == "Darwin": # fix for macOS returning "Darwin"
                current_os = "macOS"
            
            # Only include items for current OS
            if os_type.lower() == current_os.lower():
                if product not in products:
                    products[product] = []
                    
                products[product].append({
                    "title": item.find("title").text,
                    "version": item.find("version").text,
                    "url": item.find("link").text,
                    "date": item.find("pubDate").text
                })
        
        return products

    def download_installer(self, url: str, product: str, version: str) -> str:
        # Download the installer file
        
        # Get original filename from URL
        filename = url.split('/')[-1]
        filepath = os.path.join(self.downloads_dir, filename)
        
        print(f"Downloading {filename}...")
        try:
            # set timeout to 10 seconds
            response = requests.get(url, stream=True, timeout=10)
            response.raise_for_status()  # check HTTP return code 200

            total_size = int(response.headers.get('content-length', 0))
            
            with open(filepath, 'wb') as f:
                for chunk in response.iter_content(chunk_size=8192):
                    if chunk:
                        f.write(chunk)
            
            print(f"Download complete: {filename}")
            return filepath

        except requests.exceptions.Timeout:
            error_msg = f"Download timed out after 10 seconds"
            print(f"Error: {error_msg}")
            raise Exception(error_msg)

        except requests.exceptions.ConnectionError as e:
            if "ProxyError" in str(e):
                error_msg = "Unable to connect through proxy"
            elif "SSLError" in str(e):
                error_msg = "SSL/TLS connection failed"
            elif "ConnectTimeout" in str(e):
                error_msg = "Connection timed out"
            else:
                error_msg = "Network connection failed"
            print(f"Error: {error_msg} - {str(e)}")
            raise Exception(error_msg)

        except requests.exceptions.HTTPError as e:
            status_code = e.response.status_code
            if status_code == 404:
                error_msg = "File not found on server"
            elif status_code == 403:
                error_msg = f"Access denied, you may try to download manually: {url}"
            elif status_code >= 500:
                error_msg = "Server error"
            else:
                error_msg = f"HTTP error {status_code}"
            print(f"Error: {error_msg} - {str(e)}")
            raise Exception(error_msg)

        except requests.exceptions.RequestException as e:
            error_msg = f"Download failed: {str(e)}"
            print(f"Error: {error_msg}")
            raise Exception(error_msg)

    def install(self, installer_path: str):
        # Run the installer with elevation on Windows, show in folder on other platforms
        print(f"Accessing installer: {installer_path}")
        try:
            if platform.system() == "Windows":
                # Use ShellExecuteW to run with elevation on Windows
                result = ctypes.windll.shell32.ShellExecuteW(
                    None,           # parent window handle
                    "runas",       # operation
                    installer_path, # file
                    None,          # parameters
                    None,          # working directory
                    1              # show window command (SW_SHOWNORMAL)
                )
                
                if result <= 32:  # ShellExecute returns a value <= 32 on error
                    raise Exception(f"Failed to launch installer (error code: {result})")
            
            elif platform.system() in ["Linux", "Darwin"]:  # Linux and macOS
                # Show file in file manager
                folder_path = os.path.dirname(installer_path)
                if platform.system() == "Linux":
                    subprocess.run(["xdg-open", folder_path])
                else:  # Darwin (macOS)
                    subprocess.run(["open", "-R", installer_path])
                
        except Exception as e:
            print(f"Error accessing installer: {e}")
            raise

class MplabInstallerGUI:
    def __init__(self, root):
        self.root = root
        self.root.title(f"{APP_NAME} v{APP_VERSION} - {APP_AUTHOR}")
        self.installer = MplabInstaller()
        self.selected_items = []
        
        # Create main frame
        self.main_frame = ttk.Frame(root, padding="10")
        self.main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # Create treeview with additional hidden url column
        self.tree = ttk.Treeview(self.main_frame, 
                                columns=('Title', 'Version', 'Date', 'url', 'product'), 
                                selectmode='none',
                                displaycolumns=('Title', 'Version', 'Date'))
        self.tree.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # Configure treeview columns
        self.tree.heading('#0', text='Select')
        self.tree.heading('Title', text='Title', command=lambda: self.sort_tree('Title', False))
        self.tree.heading('Version', text='Version', command=lambda: self.sort_tree('Version', False))
        self.tree.heading('Date', text='Publish Date', command=lambda: self.sort_tree('Date', True))
        # Add sort state tracking
        self.sort_column = 'Date'  # Default sort column
        self.sort_reverse = True   # Default sort direction

        self.tree.column('#0', width=50)  # Checkbox column
        self.tree.column('Title', width=300)
        self.tree.column('Version', width=100, anchor='center')
        self.tree.column('Date', width=200, anchor='center')
        
        # Add tag configuration for downloaded items
        self.tree.tag_configure('downloaded', foreground='blue')
        self.tree.tag_configure('error', foreground='red')
        if self.is_dark_mode():
            self.tree.tag_configure('normal', foreground='white')
        else:
            self.tree.tag_configure('normal', foreground='black')
        self.tree.tag_configure('selected', background='lightblue')
        self.tree.tag_configure('installed', foreground='green')

        # Add scrollbar
        scrollbar = ttk.Scrollbar(self.main_frame, orient=tk.VERTICAL, command=self.tree.yview)
        scrollbar.grid(row=0, column=2, sticky=(tk.N, tk.S))
        self.tree.configure(yscrollcommand=scrollbar.set)
        
        # Add buttons
        btn_frame = ttk.Frame(self.main_frame)
        btn_frame.grid(row=1, column=0, columnspan=2, pady=10)
        
        ttk.Button(btn_frame, text="Download", command=self.download_selected).pack(side=tk.LEFT, padx=5)
        install_btn = ttk.Button(btn_frame, text="Install", command=self.install_selected).pack(side=tk.LEFT, padx=5)
        ttk.Button(btn_frame, text="Refresh", command=self.populate_tree).pack(side=tk.LEFT, padx=5)
        
        # Add status labels
        status_frame = ttk.Frame(self.main_frame)
        status_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E))
        
        # Add download path label
        path_label = ttk.Label(status_frame, text=f"Download path: {self.installer.downloads_dir}")
        path_label.pack(side=tk.TOP, anchor=tk.W, pady=(0, 5))
        
        # Initialize status system with list for errors
        self.status_var = tk.StringVar()
        self.status_queue = queue.Queue()
        self.error_messages = []
        
        # Create text widget for multi-line status
        self.status_text = tk.Text(status_frame, height=4, wrap=tk.WORD)
        self.status_text.pack(side=tk.TOP, fill=tk.X, expand=True)

        # Start status queue checker
        self.check_status_queue()
        
        # Bind checkbox click
        self.tree.bind('<Button-1>', self.handle_click)

        # create a queue for tag updates
        self.tree_tag_queue = queue.Queue()
        self.check_tree_tag_queue()
        
        # Populate tree
        self.populate_tree()

    def update_status(self, message: str, is_error=False):
        """Safely update status from any thread"""
        if is_error:
            self.status_queue.put(("error", message))
        else:
            self.status_queue.put(("info", message))

    def check_status_queue(self):
        """Check status queue and update UI"""
        try:
            while True:
                msg_type, message = self.status_queue.get_nowait()
                if msg_type == "error":
                    self.error_messages.append(message)
                    # Show all errors and current status
                    status_text = "\n".join(self.error_messages)
                else:
                    # Show current status and all errors
                    if self.error_messages:
                        status_text = f"{message}\n" + "\n".join(self.error_messages)
                    else:
                        status_text = f"{message}"
                
                self.status_text.delete('1.0', tk.END)
                self.status_text.insert('1.0', status_text)
        except queue.Empty:
            pass
        finally:
            self.root.after(100, self.check_status_queue)   

    def update_tree_tag_status(self, item_id, tags_set):
        """Safely update item tag from any thread"""
        self.tree_tag_queue.put((item_id, tags_set))

    def check_tree_tag_queue(self):
        """Check tree view color queue and update UI"""
        try:
            while True:
                item_id, tags_set = self.tree_tag_queue.get_nowait()
                self.tree.item(item_id, tags=(tags_set,))
        except queue.Empty:
            pass
        finally:
            self.root.after(100, self.check_tree_tag_queue)  

    def is_dark_mode(self):
        #  Detect OS Dark Mode
        if platform.system() == "Windows":  # Windows
            try:
                import winreg
                with winreg.OpenKey(
                    winreg.HKEY_CURRENT_USER,
                    r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"
                ) as key:
                    # 0 = Light Mode, 1 = Dark Mode
                    value, _ = winreg.QueryValueEx(key, "AppsUseLightTheme")
                    return value == 0
            except FileNotFoundError:
                return False
        elif platform.system() == "Darwin":  # macOS
            try:
                result = subprocess.run(
                    ["defaults", "read", "-g", "AppleInterfaceStyle"],
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    text=True
                )
                return "Dark" in result.stdout
            except Exception:
                return False
        else:  # Linux
            # Implement dark mode detection for Linux
            return False

        return False

    def sort_tree(self, column, is_date=False):
        """Sort tree contents when a column header is clicked"""
        # Get all items
        items = [(self.tree.set(item, column), item) for item in self.tree.get_children('')]
        
        # If clicking the same column, reverse the sort order
        if self.sort_column == column:
            self.sort_reverse = not self.sort_reverse
        else:
            self.sort_column = column
            self.sort_reverse = False
        
        # Sort items
        if is_date:
            # Parse dates for proper sorting
            items.sort(key=lambda x: datetime.strptime(x[0], "%Y-%m-%d"), reverse=self.sort_reverse)
        else:
            # Regular string sorting
            items.sort(key=lambda x: x[0].lower(), reverse=self.sort_reverse)
        
        # Rearrange items in sorted positions
        for index, (_, item) in enumerate(items):
            self.tree.move(item, '', index)
        
        # Add sort indicator to column header
        for col in ('Title', 'Version', 'Date'):
            if col == column:
                indicator = " ▼" if self.sort_reverse else " ▲"
                self.tree.heading(col, text=f"{col}{indicator}")
            else:
                # Remove sort indicator from other columns
                self.tree.heading(col, text=col)

    def mark_as_downloaded(self, item_id):
        """Mark an item as downloaded by adding a green color tag"""
        self.update_tree_tag_status(item_id, 'downloaded')
    
    def mark_as_selected(self, item_id):
        """Mark an item as downloaded by adding a green color tag"""
        self.update_tree_tag_status(item_id, 'selected')

    def mark_as_normal(self, item_id):
        """Mark an item as downloaded by adding a green color tag"""
        self.update_tree_tag_status(item_id, 'normal')

    def mark_as_error(self, item_id):
        """Mark an item as downloaded by adding a green color tag"""
        self.update_tree_tag_status(item_id, 'error')

    def mark_as_installed(self, item_id):
        """Mark an item as downloaded by adding a green color tag"""
        self.update_tree_tag_status(item_id, 'installed')

    def populate_tree(self):
        # Clear error msg
        self.error_messages = []
        # Clear existing items
        for item in self.tree.get_children():
            self.tree.delete(item)
        
        products = self.installer.get_available_products()
        
        # Flatten the product versions into a single list
        for product, versions in products.items():
            for version in versions:
                # Parse and format the date
                date_str = version['date']
                try:
                    # Parse ISO format date string
                    date_obj = datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%f%z")
                    # Format to show only date
                    formatted_date = date_obj.strftime("%Y-%m-%d")
                except ValueError:
                    formatted_date = date_str  # Keep original if parsing fails

                item_id = self.tree.insert('', 'end', text='', 
                                        values=(version['title'], 
                                                version['version'], 
                                                formatted_date))
                # Store url directly in the item
                self.tree.set(item_id, 'url', version['url'])
                self.tree.set(item_id, 'product', {product})
                self.mark_as_normal(item_id)
        self.update_status("Ready: Select items to download/install")

    def handle_click(self, event):
        region = self.tree.identify("region", event.x, event.y)
        if region == "tree":
            item = self.tree.identify_row(event.y)
            if item:  # No need to check for leaf nodes since we have a flat list
                current_text = self.tree.item(item)["text"]
                self.tree.item(item, text="✓" if not current_text else "")
                if current_text == "":
                    self.mark_as_selected(item)
                else:
                    self.mark_as_normal(item)

    def download_selected(self):
        selected = [item for item in self.tree.get_children() 
                   if self.tree.item(item)["text"] == "✓"]
        
        # Clear error msg
        self.error_messages = []

        if not selected:
            self.update_status("Please select items to download")
            return
        
        download_item_buf = selected
        download_detail_buf = {item: self.tree.item(item).get('values', []) for item in selected}
        print(f"Selected items for download: {selected}")
        print("Download detail buffer:")
        for item in download_item_buf:
            print(f"{item}:{download_detail_buf[item]}")
            
        def download_worker(item):
            url = download_detail_buf[item][3]
            title = download_detail_buf[item][0]
            version = download_detail_buf[item][1]
            
            self.update_status(f"Downloading {title} v{version}...")
            installer_path = self.installer.download_installer(url, title.strip(), version)
            # Use after() to safely update UI from worker thread
            self.mark_as_downloaded(item)
            return (title, version, installer_path)
            
        def process_downloads():
            try:
                # Start downloads in parallel
                with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
                    future_to_item = {executor.submit(download_worker, item): item 
                                    for item in download_item_buf}
                    
                    # As each download completes
                    for future in concurrent.futures.as_completed(future_to_item):
                        try:
                            title, version, _ = future.result()
                            self.update_status(f"Downloaded {title} v{version}")
                        except Exception as e:
                            item = future_to_item[future]
                            error_msg = f"Error downloading {download_detail_buf[item][0]}: {str(e)}"
                            self.update_status(error_msg, is_error=True)
                            self.mark_as_error(item)
                    
                self.update_status("All downloads complete: Blue - Success, Red - Error")
            except Exception as e:
                self.update_status(f"Error: Downloading process - {str(e)}", is_error=True)

        # Run downloads in separate thread
        thread = threading.Thread(target=process_downloads)
        thread.daemon = True  # Make thread daemon so it won't prevent program exit
        thread.start()

    def install_selected(self):
        selected = [item for item in self.tree.get_children() 
                   if self.tree.item(item)["text"] == "✓"]
        
        # Clear error msg
        self.error_messages = []

        if not selected:
            self.update_status("Please select items to install")
            return
        
        selected_buf = selected
        tree_view_buf = {item: self.tree.item(item).get('values', []) for item in selected}

        print(f"Selected items for installation: {selected}")
        print("Tree view buffer:")
        for item in selected:
            print(f"{item}:{tree_view_buf[item]}")

        def process_installations():
            try:
                for i, item in enumerate(selected_buf, 1):
                    try:
                        print(f"Processing item: {item}")
                        title = tree_view_buf[item][0]
                        version = tree_view_buf[item][1]
                        url = tree_view_buf[item][3]
                        
                        # Get the downloaded file path
                        filename = url.split('/')[-1]
                        installer_path = os.path.join(self.installer.downloads_dir, filename)
                        
                        if not os.path.exists(installer_path):
                            self.update_status(f"Error: {title} v{version} not downloaded yet", is_error=True)
                            self.mark_as_error(item)
                            continue
                    
                        self.update_status(f"[{i}/{len(selected)}] Installing {title} v{version}...")
                        self.installer.install(installer_path)
                    except Exception as e:
                        # Make sure 'title' is initialized even if an error occurs
                        error_title = title if 'title' in locals() else "Unknown"
                        error_msg = f"Error: {error_title} - {str(e)}"
                        self.update_status(error_msg, is_error=True)
                        if error_title != "Unknown":
                            self.mark_as_error(item)
                        continue

                    self.mark_as_installed(item)
                
                if platform.system() == "Windows":
                    self.update_status("Installations complete: Green - Installer Launched, Red - Error")
                else:
                    self.update_status("Open the folder to of installers for you...")
            except Exception as e:
                self.update_status(f"Error: processing installations - {str(e)}", is_error=True)

        # Run installations in separate thread
        thread = threading.Thread(target=process_installations)
        thread.daemon = True
        thread.start()

def main():
    root = tk.Tk()
    app = MplabInstallerGUI(root)
    root.mainloop()

if __name__ == "__main__":
    main()